上一篇看 HolmesGPT 的 Tool 設計時,重點是 Model 這一輪到底有哪些能力可以用。
這一篇換成 Memory。
HolmesGPT 沒有另外包一個很抽象的 MemoryManager。真正讓 Agent 延續上下文的核心,其實就是送給 LLM 的:
messages
例如:
system
user
assistant
tool
assistant
user
...
其中ConversationWorker把原本只存在 LLM Context 裡的 messages,進一步變成:
可以保存的 Checkpoint
可以在 Human Gate 後 Resume 的 State
可以壓縮後繼續使用的 Memory
可以接受 Human Feedback 後立即修正
這篇就集中看這四件事。
HolmesGPT 的 Conversation Memory 主線可以簡化成:
┌────────────────────────────────────┐
│ 使用者 / Frontend │
└─────────────────┬──────────────────┘
│ user_message
▼
┌────────────────────────────────────┐
│ ConversationEvents │
│ │
│ 對話事件儲存 │
│ │
│ holmes/core/supabase_dal.py │
│ get_conversation_events() │
└─────────────────┬──────────────────┘
│
▼
┌────────────────────────────────────┐
│ ConversationWorker │
│ │
│ 對話執行與 Memory Restore │
│ │
│ conversations_worker/worker.py │
│ _hydrate_task_from_events() │
└─────────────────┬──────────────────┘
│ conversation_history
▼
┌────────────────────────────────────┐
│ build_chat_messages() │
│ │
│ 組成本輪 messages │
│ │
│ holmes/core/conversations.py │
└─────────────────┬──────────────────┘
│
▼
┌────────────────────────────────────┐
│ ToolCallingLLM │
│ │
│ LLM → Tool → LLM │
│ │
│ holmes/core/tool_calling_llm.py │
└─────────────────┬──────────────────┘
│
▼
┌────────────────────────────────────┐
│ Terminal Event │
│ │
│ ai_answer_end │
│ approval_required │
│ │
│ 內含完整 messages Snapshot │
└─────────────────┬──────────────────┘
│
▼
┌────────────────────────────────────┐
│ ConversationEventPublisher │
│ │
│ 寫回 ConversationEvents │
│ │
│ event_publisher.py │
└─────────────────┬──────────────────┘
│
└────→ 下一輪重新 Restore
這張圖先記住三個角色就夠了:
messages:Agent 真正使用的 Memory。ConversationWorker:把之前保存的 Memory 恢復回來。Terminal Event:把目前的 messages 保存成新的 Checkpoint。後面的 Approval、Compaction、Feedback,都是圍繞這份 messages 發生。
HolmesGPT 第一個值得看的設計,是:
一輪 Agent 執行結束時,Terminal Event 會直接保存完整
messages。
關鍵 code 在:
holmes/core/conversations_worker/models.py
ConversationTask.conversation_history 的定義:
@property
def conversation_history(self):
"""Reconstructed from prior terminal events
(ai_answer_end / approval_required)."""
真正恢復 History 的地方則在:
holmes/core/conversations_worker/worker.py
└── _hydrate_task_from_events()
核心邏輯:
messages = (ev.get("data") or {}).get("messages")
if messages:
task.conversation_history = messages
也就是下一輪不是重新把所有 event 拼回 history,而是直接找到上一個 terminal event 裡保存的 messages。
假設 SRE 第一輪問:
User:
checkout-api 在 production 一直 500,可以幫我查嗎?
Assistant:
先查 Pod。
Tool:
RestartCount = 6
Assistant:
再查 Logs。
Tool:
java.lang.OutOfMemoryError
Assistant:
再查 Deployment。
Tool:
memory limit = 512Mi
Assistant:
目前看起來是 OOM。
這一輪結束後:
ai_answer_end
│
└── messages
├─ User:checkout-api 一直 500
├─ Tool:RestartCount = 6
├─ Tool:OutOfMemoryError
├─ Tool:memory limit = 512Mi
└─ Assistant:目前看起來是 OOM
下一輪:
User:
剛才那個服務重啟幾次?
↓
ConversationWorker
restore 上一個 ai_answer_end.messages
↓
Model 仍看得到:
RestartCount = 6
↓
Assistant:
6 次
所以 ai_answer_end 不只是「這輪回答完成」。
它同時是一份:
可以直接拿來開始下一輪的 Memory Snapshot。
approval_required = 可以 Resume 的 Execution Checkpoint第二個特色,是 HolmesGPT 保存的不只是「聊過什麼」,還包含:
Agent 執行到哪裡。
假設前面的 checkout-api 調查結束後,Model 決定:
restart deployment checkout-api
但這個 Tool 需要 Human Approval。
關鍵 code 在:
holmes/core/tool_calling_llm.py
當 Tool 需要人工確認時,APPROVAL_REQUIRED 會帶:
{
"messages": messages,
"pending_approvals": [...],
...
}
所以它保存的不是一句:
等待使用者同意
而是:
approval_required
│
├─ messages
│ └─ 完整 Agent Context
│
└─ pending_approvals
└─ restart_deployment(...)
ConversationWorker 也特別支援沒有新問題、只有 Tool Decision 的 Resume:
resume_only = bool(
not ask and (
data.get("tool_decisions")
or data.get("frontend_tool_results")
)
)
如果是 Resume:
if resume_only and chat_request.conversation_history:
messages = list(chat_request.conversation_history)
HolmesGPT 查到 checkout-api OOM
↓
Model 準備呼叫:
restart_deployment(
namespace="production",
deployment="checkout-api"
)
↓
approval_required
↓
保存:
messages + pending_approvals
↓
Human:Approve
↓
只送 tool_decisions
沒有新的 User Question
↓
ConversationWorker
Restore 原本 messages
↓
執行原本的 restart_deployment()
↓
Agent 繼續後面的處理
所以 HolmesGPT 的第二個特色可以濃縮成:
Conversation Memory 同時也是 Agent Execution State。
這對需要 Human Gate 的 Agent 特別重要。
Agent 跑久之後,messages 一定會越來越長。
HolmesGPT 在:
holmes/core/truncation/input_context_window_limiter.py
每次 LLM Call 前都可以進入:
compact_if_necessary()
真正負責壓縮的是:
holmes/core/truncation/compaction.py
└── compact_conversation_history()
目前 Compaction 後會保留:
System Prompt
+
Conversation Summary
+
Last User Prompt
例如原本 History 已經包含:
checkout-api 500
Pod RestartCount = 6
Logs = OutOfMemoryError
memory limit = 512Mi
HPA max replicas = 3
Node memory pressure = false
...
Compaction 後可能變成:
System:
原本 HolmesGPT System Prompt
User:
Conversation summary:
- production checkout-api 持續 500
- Pod 已 Restart 6 次
- Logs 出現 OutOfMemoryError
- memory limit = 512Mi
- HPA max replicas = 3
- Node 沒有 memory pressure
- 目前推測為 container memory limit
User:
那為什麼昨天沒發生?
真正重要的是:壓縮後不只是拿來完成「這一次」LLM Call。
compact_if_necessary() 會發出:
conversation_history_compacted
其中直接包含:
"messages": compaction_result.messages_after_compaction
而:
holmes/core/conversations_worker/event_publisher.py
把它和另外兩種 Snapshot Event 放在一起:
_COMPACT_ON_FLUSH_EVENTS = {
StreamEvents.ANSWER_END,
StreamEvents.APPROVAL_REQUIRED,
StreamEvents.CONVERSATION_HISTORY_COMPACTED,
}
查了 Pod / Logs / HPA / Deployment / Node...
↓
messages 太長
↓
compact_conversation_history()
↓
產生 Summary
↓
重新組成較短的 messages
↓
conversation_history_compacted
↓
這份 compacted messages
成為新的 Memory Snapshot
↓
之後 Conversation 從這一版繼續
因此 HolmesGPT 的 Compaction 不只是:
減少 Token。
它同時也是:
Memory State Transition。
也就是 Model 現在看到的 History 被壓縮後,Backend 後面恢復的基準也一起更新。
HolmesGPT 的 Feedback 有兩種,而且用途完全不同。
這兩條不應該混在一起看。
第一種是一般產品常見的:
👍 / 👎
category
comment
每次 HolmesGPT request 都有自己的request_id, 用來將feedback跟對話關聯(相關 code:holmes/core/usage_recorder.py)
註解:
FE saves it from ai_answer_end
and passes it to the
record_feedback Supabase RPC later
when the user clicks 👍/👎.
holmes/core/supabase_dal.py 也明確寫:
feedback writes
(thumbs up/down + category + comment)
do NOT go through Holmes.
HolmesGPT:
checkout-api 的主要問題是 memory limit 過低
↓
User:👎
Category:
Incorrect root cause
Comment:
昨晚剛部署新版,
也應該檢查 Deployment Change
↓
Frontend
record_feedback()
↓
Supabase
↓
Evaluation / Analytics
這份 Feedback 不會直接塞回 Agent Context。
所以這是一條:
Out-of-band Feedback。
messages另一種 Feedback 發生在 Tool Approval。
ToolApprovalDecision 裡有:
tool_call_id: str
approved: bool
feedback: Optional[str] = None
真正處理拒絕原因的地方是:
holmes/core/tool_calling_llm.py
└── _execute_tool_decisions()
如果使用者拒絕 Tool:
feedback_text = (
f" User feedback: {tool_decision.feedback}"
if tool_decision and tool_decision.feedback
else ""
)
error_text = (
f"Tool execution was denied by the user."
f"{feedback_text}"
)
接著 Tool Result 會被轉成 LLM Message:
tool_call_message = tool_result.to_llm_message(...)
再插回:
messages.insert(...)
Assistant:
我要 restart production checkout-api
↓
Human:Reject
Feedback:
Production 先不要 Restart,
先檢查昨晚 Deployment 的 image version
↓
Tool Result:
Tool execution was denied by the user.
User feedback:
Production 先不要 Restart,
先檢查昨晚 Deployment 的 image version
↓
這筆 Tool Result 插回 messages
↓
下一次 LLM Call
↓
Assistant:
先查看 Deployment revision
與 image version
Repo 的:
tests/test_tool_calling_llm.py
也直接檢查 Tool Message 是否包含:
User feedback: try using namespace kube-system instead
所以這條 Feedback 不是純紀錄。
它會直接改變 Agent 下一步看到的 observation。
這是一條:
In-band Corrective Feedback。
User Feedback
│
┌───────────┴───────────┐
│ │
▼ ▼
Product Feedback Tool Correction
│ │
👍 / 👎 / comment Reject + feedback
│ │
▼ ▼
record_feedback() ToolApprovalDecision
│ │
▼ ▼
Supabase _execute_tool_decisions()
│ │
▼ ▼
Evaluation / Analytics Tool Message
│
▼
messages
│
▼
下一次 Model Call